8.3.1 解码JSON

我们要学习的处理JSON的第一个方面是,使用 json 包的 NewDecoder 函数以及 Decode 方法进行解码。如果要处理来自网络响应或者文件的JSON,那么一定会用到这个函数及方法。让我们来看一个处理 Get 请求响应的JSON的例子,这个例子使用 http 包获取Google搜索API返回的JSON。代码清单8-23展示了这个响应的内容。

代码清单8-23 Google搜索API的JSON响应例子

{
  "responseData": {
    "results": [
      {
        "GsearchResultClass": "GwebSearch",
        "unescapedUrl": "https://www.reddit.com/r/golang",
        "url": "https://www.reddit.com/r/golang",
        "visibleUrl": "www.reddit.com",
        "cacheUrl": "http://www.google.com/search?q=cache:W...",
        "title": "r/\u003cb\u003eGolang\u003c/b\u003e - Reddit",
        "titleNoFormatting": "r/Golang - Reddit",
        "content": "First Open Source \u003cb\u003eGolang\u..."
      },
      {
        "GsearchResultClass": "GwebSearch",
        "unescapedUrl": "http://tour.golang.org/",
        "url": "http://tour.golang.org/",
        "visibleUrl": "tour.golang.org",
        "cacheUrl": "http://www.google.com/search?q=cache:O...",
        "title": "A Tour of Go",
        "titleNoFormatting": "A Tour of Go",
        "content": "Welcome to a tour of the Go programming ..."
      }
    ]
  }
}

代码清单8-24给出的是如何获取响应并将其解码到一个结构类型里的例子。

代码清单8-24 listing24.go

01 // 这个示例程序展示如何使用json包和NewDecoder函数
02 // 来解码JSON响应
03 package main
04
05 import (
06   "encoding/json"
07   "fmt"
08   "log"
09   "net/http"
10 )
11
12 type (
13   // gResult映射到从搜索拿到的结果文档
14   gResult struct {
15     GsearchResultClass string `json:"GsearchResultClass"`
16     UnescapedURL    string `json:"unescapedUrl"`
17     URL         string `json:"url"`
18     VisibleURL     string `json:"visibleUrl"`
19     CacheURL       string `json:"cacheUrl"`
20     Title        string `json:"title"`
21     TitleNoFormatting string `json:"titleNoFormatting"`
22     Content       string `json:"content"`
23   }
24
25   // gResponse包含顶级的文档
26   gResponse struct {
27     ResponseData struct {
28       Results []gResult `json:"results"`
29     } `json:"responseData"`
30   }
31 )
32
33 func main() {
34   uri := "http://ajax.googleapis.com/ajax/services/search/web?v=1.0&rsz=8&q=golang"
35
36   // 向Google发起搜索
37   resp, err := http.Get(uri)
38   if err != nil {
39     log.Println("ERROR:", err)
40     return
41   }
42   defer resp.Body.Close()
43
44   // 将JSON响应解码到结构类型
45   var gr gResponse
46   err = json.NewDecoder(resp.Body).Decode(&gr)
47   if err != nil {
48     log.Println("ERROR:", err)
49     return
50   }
51
52   fmt.Println(gr)
53 }

代码清单8-24中代码的第37行,展示了程序做了一个HTTP Get 调用,希望从Google得到一个JSON文档。之后,在第46行使用 NewDecoder 函数和 Decode 方法,将响应返回的JSON文档解码到第26行声明的一个结构类型的变量里。在第52行,将这个变量的值写到 stdout

如果仔细看第26行和第14行的 gResponsegResult 的类型声明,你会注意到每个字段最后使用单引号声明了一个字符串。这些字符串被称作 标签 (tag),是提供每个字段的元信息的一种机制,将JSON文档和结构类型里的字段一一映射起来。如果不存在 标签 ,编码和解码过程会试图以大小写无关的方式,直接使用字段的名字进行匹配。如果无法匹配,对应的结构类型里的字段就包含其零值。

执行HTTP Get 调用和解码JSON到结构类型的具体技术细节都由标准库包办了。让我们看一下标准库里 NewDecoder 函数和 Decode 方法的声明,如代码清单8-25所示。

代码清单8-25 golang.org/src/encoding/json/stream.go

// NewDecoder返回从r读取的解码器
//
// 解码器自己会进行缓冲,而且可能会从r读比解码JSON值
// 所需的更多的数据
func NewDecoder(r io.Reader) *Decoder
// Decode从自己的输入里读取下一个编码好的JSON值, 
// 并存入v所指向的值里
//
// 要知道从JSON转换为Go的值的细节, 
// 请查看Unmarshal的文档
func (dec *Decoder) Decode(v interface{}) error

在代码清单8-25中可以看到 NewDecoder 函数接受一个实现了 io.Reader 接口类型的值作为参数。在下一节,我们会更详细地介绍 io.Readerio.Writer 接口,现在只需要知道标准库里的许多不同类型,包括 http 包里的一些类型,都实现了这些接口就行。只要类型实现了这些接口,就可以自动获得许多功能的支持。

函数 NewDecoder 返回一个指向 Decoder 类型的指针值。由于Go语言支持复合语句调用,可以直接调用从 NewDecoder 函数返回的值的 Decode 方法,而不用把这个返回值存入变量。在代码清单8-25里,可以看到 Decode 方法接受一个 interface{} 类型的值做参数,并返回一个 error 值。

在第5章中曾讨论过,任何类型都实现了一个空接口 interface{} 。这意味着 Decode 方法可以接受任意类型的值。使用反射, Decode 方法会拿到传入值的类型信息。然后,在读取JSON响应的过程中, Decode 方法会将对应的响应解码为这个类型的值。这意味着用户不需要创建对应的值, Decode 会为用户做这件事情,如代码清单8-26所示。

在代码清单8-26中,我们向 Decode 方法传入了指向 gResponse 类型的指针变量的地址,而这个地址的实际值为 nil 。该方法调用后,这个指针变量会被赋给一个 gResponse 类型的值,并根据解码后的JSON文档做初始化。

代码清单8-26 使用 Decode 方法

var gr *gResponse
err = json.NewDecoder(resp.Body).Decode(&gr)

有时,需要处理的JSON文档会以 string 的形式存在。在这种情况下,需要将 string 转换为 byte 切片( []byte ),并使用 json 包的 Unmarshal 函数进行反序列化的处理,如代码清单8-27所示。

代码清单8-27 listing27.go

01 // 这个示例程序展示如何解码JSON字符串
02 package main
03
04 import (
05   "encoding/json"
06   "fmt"
07   "log"
08 )
09
10 // Contact结构代表我们的JSON字符串
11 type Contact struct {
12   Name  string `json:"name"`
13   Title  string `json:"title"`
14   Contact struct {
15     Home string `json:"home"`
16     Cell string `json:"cell"`
17   } `json:"contact"`
18 }
19
20 // JSON包含用于反序列化的演示字符串
21 var JSON = `{
22   "name": "Gopher",
23   "title": "programmer",
24   "contact": {
25     "home": "415.333.3333",
26     "cell": "415.555.5555"
27   }
28 }`
29
30 func main() {
31   // 将JSON字符串反序列化到变量
32   var c Contact
33   err := json.Unmarshal([]byte(JSON), &c)
34   if err != nil {
35     log.Println("ERROR:", err)
36     return
37   }
38
39   fmt.Println(c)
40 }

在代码清单8-27中,我们的例子将JSON文档保存在一个字符串变量里,并使用 Unmarshal 函数将JSON文档解码到一个结构类型的值里。如果运行这个程序,会得到代码清单8-28所示的输出。

代码清单8-28 listing27.go的输出

{Gopher programmer {415.333.3333 415.555.5555}}

有时,无法为JSON的格式声明一个结构类型,而是需要更加灵活的方式来处理JSON文档。在这种情况下,可以将JSON文档解码到一个 map 变量中,如代码清单8-29所示。

代码清单8-29 listing29.go

01 // 这个示例程序展示如何解码JSON字符串
02 package main
03
04 import (
05   "encoding/json"
06   "fmt"
07   "log"
08 )
09
10 // JSON包含要反序列化的样例字符串
11 var JSON = `{
12   "name": "Gopher",
13   "title": "programmer",
14   "contact": {
15     "home": "415.333.3333",
16     "cell": "415.555.5555"
17   }
18 }`
19
20 func main() {
21   // 将JSON字符串反序列化到map变量
22   var c map[string]interface{}
23   err := json.Unmarshal([]byte(JSON), &c)
24   if err != nil {
25     log.Println("ERROR:", err)
26     return
27   }
28
29   fmt.Println("Name:", c["name"])
30   fmt.Println("Title:", c["title"])
31   fmt.Println("Contact")
32   fmt.Println("H:", c["contact"].(map[string]interface{})["home"])
33   fmt.Println("C:", c["contact"].(map[string]interface{})["cell"])
34 }

代码清单8-29中的程序修改自代码清单8-27,将其中的结构类型变量替换为 map 类型的变量。变量 c 声明为一个 map 类型,其键是 string 类型,其值是 interface{} 类型。这意味着这个 map 类型可以使用任意类型的值作为给定键的值。虽然这种方法为处理JSON文档带来了很大的灵活性,但是却有一个小缺点。让我们看一下访问 contact 子文档的 home 字段的代码,如代码清单8-30所示。

代码清单8-30 访问解组后的映射的字段的代码

fmt.Println("\tHome:", c["contact"].(map[string]interface{})["home"])

因为每个键的值的类型都是 interface{} ,所以必须将值转换为合适的类型,才能处理这个值。代码清单8-30展示了如何将 contact 键的值转换为另一个键是 string 类型,值是 interface{} 类型的 map 类型。这有时会使映射里包含另一个文档的JSON文档处理起来不那么友好。但是,如果不需要深入正在处理的JSON文档,或者只打算做很少的处理,因为不需要声明新的类型,使用 map 类型会很快。

results matching ""

    No results matching ""